/*
   Copyright 2009 Abdulla Abdurakhmanov (abdulla@latestbit.com)
   Original sources are available at www.latestbit.com

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package org.latestbit.smartobjects.network;

import java.io.IOException;
import java.net.URI;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.latestbit.smartobjects.exceptions.IncorrectURIException;

public class ManagedClient extends ManagedConnection implements IManagedConnectionFactory, IManagedClient {	
	
	private Map<String, IConnector> connectors = new HashMap < String, IConnector >();
	private Map<ConnectionHandler, IConnector> allHandlers = new HashMap < ConnectionHandler, IConnector> ();
	
	private INetworkFactory factory;
	private boolean started = false;
	
	private Lock awaitConnectLock = new ReentrantLock();
	
	private Condition awaitConnectCondition = awaitConnectLock.newCondition();
	
	
	public ManagedClient(INetworkFactory factory) {
		this.factory = factory;
	}

	/* (non-Javadoc)
	 * @see org.latestbit.s2bi.network.IManagedClient#setEndpointReference(org.latestbit.s2bi.IEndpointReference)
	 */
	public void setEndpointLocation(IEndpointLocation location, IEndpointCoordinator coordinator) throws IncorrectURIException, IOException {
		synchronized(connectors) {
			if(this.endpointCoordinator!=null)
				this.endpointCoordinator.setManagedConnectionFactory(null);
			this.endpointLocation = location;
			this.endpointCoordinator = coordinator;
			this.endpointCoordinator.setManagedConnectionFactory(this);
		}
	}	
	
	/* (non-Javadoc)
	 * @see org.latestbit.s2bi.network.IManagedClient#start()
	 */
	@Override
	public void start() throws IncorrectURIException, IOException {
		synchronized(connectors) {
			updateConnectors();
			for ( IConnector connector : connectors.values() ) {
				connector.connectAsync();
			}
			started = true;
		}
	}
	
	public void start(long awaitConnectTime, TimeUnit awaitConnectTimeUnit) throws IncorrectURIException, IOException, InterruptedException {
		awaitConnectLock.lock();
		try {
			start();		
			if(!awaitConnectCondition.await(awaitConnectTime, awaitConnectTimeUnit)) {
				throw new IOException("Timeout to connect to any specified endpoints");
			}
		}
		finally {
			awaitConnectLock.unlock();
		}
	}
	
	/* (non-Javadoc)
	 * @see org.latestbit.s2bi.network.IManagedClient#stop()
	 */
	@Override
	public void stop() {
		Map<String, IConnector> tempConnectors = new HashMap < String, IConnector >();
		synchronized(connectors) {
			started = false;
			connectionStatus = false;			
			tempConnectors.putAll (connectors);
			connectors.clear();
		}

		for(IConnector connector : tempConnectors.values()) {
			connector.close();
			connector.destroy();				
		}		
		
		synchronized(connectors) {
			for(ConnectionHandler handler : allHandlers.keySet()) {
				handler.getConnection().delConnectionListener(handler);
				handler.getConnection().delStreamListener(handler);
				if(endpointCoordinator!=null)
					endpointCoordinator.onRemoved(handler);
			}
			allHandlers.clear();
		}
	}
	
	@Override
	public void finalize() {
		destroy();
	}
	
	@Override
	public void destroy() {
		stop();		
	}

	protected void updateConnectors() throws IncorrectURIException, IOException {
		synchronized(connectors) {
			Map<String, IConnector> connectorsTemp = new HashMap<String, IConnector>();
			connectorsTemp.putAll( connectors );			
			connectors.clear();
			for ( URI location : getEndpointLocation().getLocations() ) {
				String key = createKey ( location );
				IConnector existConnector =  connectorsTemp.get(key);
				if(existConnector!=null) {
					// Only coping an exist connector to storage
					connectors.put(key, existConnector);
				}
				else {
					// Creating a new connector
					IConnector connector = factory.createConnector();
					connector.setEndpointURI(location);
					ConnectionHandler handler = new ConnectionHandler (
							connector,
							endpointCoordinator
					);
					allHandlers.put(handler, connector);
					connector.addConnectionListener(handler);
					connector.addStreamListener(handler);

					connectors.put(key, connector);
					if( started )
						connector.connectAsync();
				}
			}
		}
	}
	
	@Override
	public void fireOnConnected() {
		super.fireOnConnected();
		awaitConnectLock.lock();
		awaitConnectCondition.signal();
		awaitConnectLock.unlock();
	}
	
	
	@Override
	public void fireOnDisconnected(ConnectionHandler handler) {
		super.fireOnDisconnected(handler);
		restoreConnection(handler);
	}

	@Override
	public void fireOnConnectionFailure(ConnectionHandler handler, String errorMessage) {
		super.fireOnConnectionFailure(handler, errorMessage);
		restoreConnection(handler);
	}

	private void restoreConnection(ConnectionHandler handler) {
		if(endpointCoordinator!=null && endpointCoordinator.onRestoreConnectionNeeded(handler)) {
			synchronized(connectors) {
				if(!started)
					return;
				IConnector connector = allHandlers.get(handler);
				if(connector!=null) {
					try {
						connector.connectAsync();
					}
					catch(Exception ex) {
						ex.printStackTrace();
					}
				}
			}
		}
	}
	
	public String createKey(URI location) {
		return location.getHost()+":"+location.getPort();
	}

	
	@Override
	public IManagedConnection createManagedConnection() {
		return this;
	}

	@Override
	public void removeManagedConnection(IManagedConnection connection) {
		// Nothing to do
	}
}
